/*
 * Copyright (c) 2010-2011, okazoh_tk. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  - Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *  - Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *  - Neither the name of the copyright holder nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include "alkes/effector/GaussianFilter.h"
#include "alkes/core/Memory.h"
#include "alkes/media/LockImageBuffer.h"
#include <math.h>
#include <string.h>

namespace alkes {

#if !defined(PI)
#define PI (3.14159265f)
#endif

#define FRAC    (12)
#define WINDOW_SIZE (16)


GaussianFilter* GaussianFilter::create(ImageBufferFactory* factory, int32_t kernel, float sigma)
{
    if (!factory)
        return 0;

    int32_t shift;
    int32_t* filter = createFilter(kernel, sigma, shift);

    if (!filter)
        return 0;

    return new GaussianFilter(factory, kernel, shift, filter);
}

void GaussianFilter::destroy()
{
    delete this;
}

GaussianFilter::GaussianFilter(ImageBufferFactory* factory, int32_t kernel, int32_t shift, int32_t* filter)
: ImageFilter(factory)
, kernel_(kernel)
, shift_(shift)
, filter_(filter)
{
}

GaussianFilter::~GaussianFilter()
{
    destroyFilter(filter_);
}

int32_t GaussianFilter::getKernel() const
{
    return kernel_;
}

const int32_t* GaussianFilter::getFilter() const
{
    return filter_;
}

int32_t GaussianFilter::getShiftValue() const
{
    return shift_;
}

bool GaussianFilter::onApply(ImageBuffer* src, intrusive_ptr<ImageBuffer>* dst)
{
    // Grayscale
    if (src->getPixelFormat() != AL_PIXEL_FORMAT_A8)
        return false;

    uint16_t width, height;
    src->getImageSize(width, height);

    uint16_t k = (uint16_t)(kernel_ - 1);
    width += k;
    height += k;

    intrusive_ptr<ImageBuffer> filtered(createImageBuffer(width, height, AL_PIXEL_FORMAT_A8));

    bool ret = false;

    if (filtered)
    {
        ret = applyFilter(src, filtered);

        if (ret)
            *dst = static_cast<ImageBuffer*>(filtered);
    }

    return ret;
}

int32_t* GaussianFilter::createFilter(int32_t kernel, float sigma, int32_t& shift)
{
    if (1 != (kernel & 1) || kernel == 1 || sigma == 0.0f)
        return 0;

    float* ffilter = (float*)alkes_malloc(kernel * sizeof(float));
    if (!ffilter)
        return 0;

    int32_t* filter = (int32_t*)alkes_malloc(kernel * sizeof(int32_t));
    if (!filter)
    {
        alkes_free(ffilter);
        return 0;
    }

    int32_t i, s;
    float sigma2 = 2 * sigma * sigma;
    float total = 0.0f;
    for (i = 0, s = -(kernel >> 1); i < kernel; ++i, ++s)
    {
        ffilter[i] = exp(-(s * s) / sigma2);
        total += ffilter[i];
    }

    shift = FRAC;
    int32_t frac = 1 << shift;

    for (i = 0; i < kernel; ++i)
        filter[i] = (int32_t)(ffilter[i] * frac / total);

    alkes_free(ffilter);

    return filter;
}

void GaussianFilter::destroyFilter(void* filter)
{
    if (filter)
    {
        alkes_free(filter);
    }
}

bool GaussianFilter::applyFilter(ImageBuffer* src, ImageBuffer* dst)
{
    WindowFilter wf;

    src->getImageSize(wf.s_width, wf.s_height);

    LockImageBufferRead read_lock(src);
    wf.src = read_lock;
    if (!wf.src)
        return false;

    wf.s_pitch = read_lock.getPitch();

    dst->getImageSize(wf.d_width, wf.d_height);

    LockImageBufferWrite write_lock(dst);
    wf.dst = write_lock;
    if (!wf.dst)
        return false;

    wf.d_pitch = write_lock.getPitch();

    int32_t cx, cy;
    int32_t ystep = WINDOW_SIZE - kernel_ - 1;
    cx = (wf.d_width + (WINDOW_SIZE-1)) / WINDOW_SIZE;
    cy = (wf.d_height + (WINDOW_SIZE-1)) / WINDOW_SIZE;

    for (wf.dy = 0; wf.dy < wf.d_height; wf.dy += ystep)
    {
        if (wf.d_height < wf.dy + WINDOW_SIZE)
            wf.h = wf.d_height - wf.dy;
        else
            wf.h = WINDOW_SIZE;

        for (wf.dx = 0; wf.dx < wf.d_width; wf.dx += WINDOW_SIZE)
        {
            if (wf.d_width < wf.dx + WINDOW_SIZE)
                wf.w = wf.d_width - wf.dx;
            else
                wf.w = WINDOW_SIZE;

            applyWindowFilter(wf);
        }
    }

    write_lock.detach();
    read_lock.detach();

    return true;
}

void GaussianFilter::applyWindowFilter(WindowFilter& wf)
{
    uint32_t work[WINDOW_SIZE * WINDOW_SIZE];
    int32_t k = kernel_ >> 1;
    int32_t y, x, i;

    memset(work, 0, sizeof(work));

    // 1. x coord
    const uint8_t* img_p_base;
    const uint8_t* img_p;
    uint32_t* work_p_base;
    uint32_t* work_p;
    int32_t* filter_p_base;
    int32_t* filter_p;

    int32_t endy;

    int32_t start_y = 0;
    int32_t start_sy = wf.dy - k;
    if (start_sy < 0)
    {
        start_y = -start_sy;
        start_sy = 0;
    }

    if (wf.s_height <= start_sy + wf.h - start_y)
        endy = start_y + (wf.s_height - start_sy);
    else
        endy = wf.h;

    int32_t start_sx, end_sx;
    int32_t start_i, iloop_count;
    int32_t yloop_count = endy - start_y;

    work_p_base = work + start_y * WINDOW_SIZE;
    start_sx = wf.dx - 2 * k;
    end_sx = start_sx + kernel_;

    for (x = 0; x < wf.w; ++x, ++work_p_base, ++start_sx, ++end_sx)
    {
        start_i = start_sx < 0 ? -start_sx : 0;

        if (wf.s_width < end_sx)
            iloop_count = (wf.s_width - start_sx) - start_i;
        else
            iloop_count = kernel_ - start_i;

        img_p_base = wf.src + start_sy * wf.s_pitch + (start_sx + start_i);
        work_p = work_p_base;
        filter_p_base = filter_ + start_i;

        for (y = 0; y < yloop_count; ++y, img_p_base += wf.s_pitch, work_p += WINDOW_SIZE)
        {
            filter_p = filter_p_base;
            img_p = img_p_base;
            for (i = 0; i < iloop_count; ++i, ++img_p, ++filter_p)
            {
                *work_p += (*filter_p * (*img_p));
            }
        }
    }

    // 2. y coord with work
    uint32_t sum;
    int32_t end_sy;
    uint8_t* dst_p_base = wf.dst + wf.dy * wf.d_pitch + wf.dx;
    start_sy = -k;
    end_sy = start_sy + kernel_;

    endy = wf.d_height <= wf.dy + wf.h ? wf.h : wf.h - k;

    if (wf.dy != 0)
    {
        y = k;
        dst_p_base += wf.d_pitch * k;
        start_sy += k;
        end_sy += k;
    }
    else
        y = 0;

    for (; y < endy; ++y, dst_p_base += wf.d_pitch, ++start_sy, ++end_sy)
    {
        start_i = start_sy < 0 ? -start_sy : 0;

        if (wf.h < end_sy)
            iloop_count = (wf.h - start_sy) - start_i;
        else
            iloop_count = kernel_ - start_i;

        work_p_base = work + (start_sy + start_i) * WINDOW_SIZE;
        filter_p_base = filter_ + start_i;

        for (x = 0; x < wf.w; ++x, ++work_p_base)
        {
            sum = 0;
            work_p = work_p_base;
            filter_p = filter_p_base;
            for (i = 0; i < iloop_count; ++i, work_p += WINDOW_SIZE, ++filter_p)
            {
                sum += (*filter_p * (*work_p));
            }
            dst_p_base[x] = (uint8_t)(sum >> (shift_ * 2));
        }
    }
}

}
